package com.bcxj.routeplan.utils;


import com.bcxj.internalcommon.util.RedisData;
import com.xiaoleilu.hutool.json.JSONObject;
import com.xiaoleilu.hutool.json.JSONUtil;
import com.xiaoleilu.hutool.util.StrUtil;
import jakarta.annotation.Resource;
import net.sf.json.util.JSONUtils;
import org.apache.commons.lang.BooleanUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;


/**
 * ClassName: RedisCacheUtils
 * Package: com.hmdp.utils
 * Description: 工具类组件, 将缓冲穿透，击穿，雪崩的解决方案继承封装。涉及所有的相关的Reids缓存一致性问题，可以直接使用这个工具类
 *
 * @Author BCXJ
 * @Create 2024/6/29 21:06
 * @Version 1.0
 * @Since 1.0
 */
@Component
public class RedisCacheUtils {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    //将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间
    public <T> void set(String key, T value, int time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }


    /**
     * 逻辑过期的数据-》redis的方法封装
     * 其实应该返回一个true/false
     * 不为null表示成功，反之表示失败
     * 超时时间以minute为单位
     * 这个方法适用于缓存穿透&击穿&雪崩，都可以使用这一套直接进行，这一个方法是三个缓存问题的综合解决方案
     * 1.0 版本存在缓存无效内容堆积问题
     * 2.0 版本解决缓存无效内容堆积问题
     */
    private <K, V> RedisData<V> saveBean2Redis(String keyPre, K id, Long expireTime, TimeUnit timeUnit,
                                               Function<K, V> function, Class<V> type) {

        try {
            expireTime += RandomTime(timeUnit); // 随机时间，防止缓存击穿
            //0 获取key
            String key = keyPre + id.toString();


            //0.2 再次尝试获取缓存数据
            String redisShopJSON = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(redisShopJSON)) {
                System.out.println(key);
                // 直接结束
                return new RedisData<>();
            }

            // 1 查询数据库数据
            V bean = function.apply(id); // 查找数据库的内容

            // 1.1. 没查到，防止缓冲穿透，redis写入null数据
            if (null == bean) {
                // 防止缓存击穿, 使用RedisData封装数据
                RedisData<V> redisData = new RedisData<V>(LocalDateTime.now().plusSeconds(expireTime), null);
                // 在逻辑过期的基础上给key设置ttl，解决redis缓存无效内容堆积问题
                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), expireTime + RandomTime(timeUnit), timeUnit);
                return redisData;
            }

            //2 封装数据
            RedisData<V> redisData = new RedisData<V>(LocalDateTime.now().plusMinutes(expireTime), bean);

            //3 存入缓存
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), expireTime + RandomTime(timeUnit), timeUnit);
            // 等待一段时间，确保set成功
            Thread.sleep(5000);
            //4 返回结果
            return redisData;
        } catch (Exception e) {
            // 5 异常处理
            e.printStackTrace();
        }
        // 6 返回null表示上面业务有问题
        return null;
    }


    /**
     *
     */

    public <T, K> T dataProcess(String redisShopJSON, Class<T> type,
                                           Function<K, T> function) {
        RedisData<T> bean = null;
        JSONObject jsonObject = JSONUtil.parseObj(redisShopJSON);
        //bean = JSONUtil.toBean(jsonObject, RedisData.class); // 涉及到泛型，所以需要指定类型，这里的工具类会自动识别

        String expireTimeStr = jsonObject.getStr("expireTime");
        LocalDateTime expireTime = LocalDateTime.parse(expireTimeStr,
                DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"));

        // 解析 data 字段的嵌套 JSON
        //String dataJson = ;
        T data = (T) jsonObject.getStr("data");
//                JSONObject jsonObject1 = JSONUtil.parseObj(data1);
//                T data = JSONUtil.toBean(jsonObject1, type);
        bean = new RedisData<T>(expireTime, data);


        // 数据没有过期(逻辑时间)
        if (LocalDateTime.now().isBefore(bean.getExpireTime())) {
            return beanByRedisData(bean);
        }

        // 下面这行是不会执行的
        return null;
    }

    /**
     * 生成一个随机时间，用于缓冲雪崩
     * 代码不完善，只写了minutes 和 seconds的解决方案
     *
     * @param timeUnit
     * @return
     */
    private Long RandomTime(TimeUnit timeUnit) {
        if (TimeUnit.MINUTES == timeUnit) {
            return (long) Math.ceil(Math.random() * 10 + 1); // 随机时间，防止缓存击穿
        } else if (TimeUnit.SECONDS == timeUnit) {
            return (long) Math.ceil(Math.random() * 600 + 1); // 随机时间，防止缓存击穿
        }

        // 这个return 是默认
        return 1L;
    }

    // 异步操作数据库线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 逻辑过期解决缓存击穿的问题
     *
     * @param keyPre   key前缀, key前缀需要加上:
     * @param id       key 后缀
     * @param type     bean的类型
     * @param time     存进redis的bean的生命长度
     * @param unit     bean的生命长度单位
     * @param lockTime 锁的时间
     * @param lockUnit 锁的时间单位
     * @param function 查询数据库的方法，利用函数式编程解决
     * @param nullTime 防止缓存穿透的空处理时间
     * @param nullUnit 防止缓存穿透的空处理时间单位
     * @param <T>      返回的类型
     * @param <K>      Key后缀类型
     * @return 返回结果
     */
    public <T, K> T queryWithLogicalExpire(String keyPre, K id, Class<T> type,
                                           Long time, TimeUnit unit,
                                           String lockKey, Long lockTime, TimeUnit lockUnit,
                                           Long nullTime, TimeUnit nullUnit,
                                           Function<K, T> function) {
        try {
            // 0 确定key
            String key = keyPre + id;
            // 1 先查询缓存, 有则判断日期
            // 1.1 没有过期，直接返回
            String redisShopJSON = stringRedisTemplate.opsForValue().get(key);
            RedisData<T> bean = null;
            if (StrUtil.isNotBlank(redisShopJSON)) {
                return dataProcess(redisShopJSON, type, function);
//                JSONObject jsonObject = JSONUtil.parseObj(redisShopJSON);
//                //bean = JSONUtil.toBean(jsonObject, RedisData.class); // 涉及到泛型，所以需要指定类型，这里的工具类会自动识别
//
//                String expireTimeStr = jsonObject.getStr("expireTime");
//                LocalDateTime expireTime = LocalDateTime.parse(expireTimeStr,
//                        DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"));
//
//                // 解析 data 字段的嵌套 JSON
//                //String dataJson = ;
//                T data = (T) jsonObject.getStr("data");
////                JSONObject jsonObject1 = JSONUtil.parseObj(data1);
////                T data = JSONUtil.toBean(jsonObject1, type);
//                bean = new RedisData<T>(expireTime, data);
//
//
//                // 数据没有过期(逻辑时间)
//                if (LocalDateTime.now().isBefore(bean.getExpireTime())) {
//                    return beanByRedisData(bean);
//                }
            }

            // 2. 过期/没有查到数据，获取锁
            if (!tryLock(lockKey, lockTime, lockUnit)) {
                // 没有获取到锁，直接返回脏数据
                return beanByRedisData(bean);
            }

            // 2.2 获取锁成功，调用数据库 -》缓存的封装解决方案的方法
            // 异步实现，采用线程池的方式
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                //重建缓存
                if (null == this.saveBean2Redis(keyPre, id, time, unit, function, type)) {
                    throw new RuntimeException("数据不存在");
                }
            });

            //3 返回数据
            return beanByRedisData(bean);
        } catch (Exception e) {
            e.printStackTrace();
            unlock(lockKey);
        } finally {
            unlock(lockKey);
        }
        throw new RuntimeException("未知错误，叫程序牛马员调bug~");
    }


    /**
     * 根据Bean返回对应的Result
     *
     * @param bean
     * @param <T>
     * @return
     */
    private <T> T beanByRedisData(RedisData<T> bean) {
        // 2.1 获取锁失败，又脏数据返回脏数据，否则直接返回错误
        if (null == bean) {
            System.out.println("redis不存在数据");
            return null;
        }
        if (null == bean.getData()) {
            System.out.println("mysql不存在数据");
            return null;
        }
        return bean.getData();
    }

    /**
     * 尝试获取锁
     *
     * @param key
     * @return
     */
    private <K> boolean tryLock(String key, Long time, TimeUnit unit) {
        //String key = keyPre + id;
        // 1 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "", time, unit);
        return BooleanUtils.isTrue(success); //防止自动拆箱出现空异常

//        RLock lock = redissonClient.getLock("anyLock");
//        //尝试获取锁，参数分别是：获取锁的最大等待时间(期间会重试)，锁自动释放时间，时间单位
//        boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//        return isLock;
    }

    /**
     * 释放锁
     */
    private <K> void unlock(String key) {
        stringRedisTemplate.delete(key);
    }


    /**
     * @param bean     传进的修改信息的bean
     * @param key      redis对应的key
     * @param function consumer 修改数据的操作
     * @param <T>      泛型-数据修改的bean的类型
     */
    public <T> Integer updateBeanById(T bean, String key, Function<T, Integer> function) {
        /**
         * 达到双写一致性，应该先操作数据库，然后删除缓存
         */
        //1 更新数据-mysql
        Integer apply = function.apply(bean);
        //2 删除缓存-redis
        stringRedisTemplate.delete(key);
        return apply;
    }


    /**
     * 根据id删除bean
     *
     * @param key      redis对应的key
     * @param id       数据的主键
     * @param function 删除数据库的函数
     * @param <T>      返回类型泛型
     * @return DML 语句都会返回一个int
     */
    public <T> Integer deleteById(String key, T id, Function<T, Integer> function) {
        // 1. 先删除数据库
        Integer apply = function.apply(id);
        // 2. 再删除缓存
        stringRedisTemplate.delete(key + id);
        return apply;
    }


    /**
     * 适用于条件删除
     *
     * @param key
     * @param bean
     * @param function
     * @param <T>
     * @return
     */
    public <T> Integer deleteByBean(String key, T bean, Function<T, Integer> function) {
        // 1. 先删除数据库
        Integer apply = function.apply(bean);
        // 2. 再删除缓存
        stringRedisTemplate.delete(key);
        return apply;
    }
}
